import { expect } from '@jest/globals';
import * as fse from 'fs-extra';
import path from 'path';

import { ModuleBuildProfile } from '../../../src/options/build/module-build-profile';
import { ProjectBuildProfile } from '../../../src/options/build/project-build-profile';
import { ConfigJson } from '../../../src/options/configure/config-json-options';
import { initExecuteCommand, runHvigor } from '../common/common';
import { ConfigJsonModifier } from '../common/modifier/config-json-modifier';
import { ModuleBuildProfileModifier } from '../common/modifier/module-build-profile-modifier';
import { ProjectBuildProfileModifier } from '../common/modifier/project-build-profile-modifier';
import { Probes } from '../common/probes';

import { Constants, PAGE_CSS_TEMPLATE, PAGE_HML_TEMPLATE, PAGE_JS_TEMPLATE, Tasks } from './constants';
import AbilitiesObj = ConfigJson.AbilitiesObj;
import JsObj = ConfigJson.JsObj;
import ModuleTargetBuildOpt = ModuleBuildProfile.ModuleTargetBuildOpt;
import ModuleBuildOpt = ProjectBuildProfile.ModuleBuildOpt;
import SigningConfigBuildOpt = ProjectBuildProfile.SigningConfigBuildOpt;


interface ModuleConfig {
  moduleName: string,
  updateTarget?: ModuleTargetBuildOpt[],
  addTarget?: ModuleTargetBuildOpt[]
}

interface ConfigJson {
  moduleName: string,
  deviceTypes?: string[]
  updateAbilities?: AbilitiesObj[]
  updateJs?: JsObj[]
}

interface ProjectConfig {
  sdkVersion?: number,
  signConfig?: SigningConfigBuildOpt,
  addTargets?: AddTarget[],
  addModules?: ModuleBuildOpt[],
  updateProducts?: any[]
}

interface AddTarget {
  moduleName: string,
  target: ProjectBuildProfile.ProjectTargetBuildOpt
}

interface BuildOption {
  signed?: boolean,
  task?: Tasks,
  rich?: boolean,
  mixed?: boolean,
  product?: string,
  moduleType?: string,
  entryModuleNames?: string[]
}


interface CheckOptions extends BuildOption {
  needPackBin: boolean;
}

interface CommandOption {
  mode: string,
  product?: string,
  module?: string
  tasks: Tasks
}

export function checkSuccessful(projectRoot: string, commandOption: CommandOption) {
  const commands = initExecuteCommand(commandOption);
  const result = runHvigor(projectRoot, commands);
  expect(result.stdout.includes('BUILD SUCCESSFUL')).toBeTruthy();
  return result;
}

export function isSubArray(targetArray: string[], array: string[]): boolean {
  return targetArray.every((item) => {
    return array.includes(item);
  });
}

function configModule(moduleConfigs: ModuleConfig[], project: string) {
  moduleConfigs?.forEach((moduleConfig) => {
    const moduleBuildProfileModifier = new ModuleBuildProfileModifier(project, moduleConfig.moduleName);

    if (moduleConfig.updateTarget?.length) {
      moduleConfig.updateTarget.forEach((config: ModuleTargetBuildOpt) => {
        moduleBuildProfileModifier.updateTarget(config);
      });
    }

    if (moduleConfig.addTarget?.length) {
      moduleConfig.addTarget.forEach((config: ModuleTargetBuildOpt) => {
        moduleBuildProfileModifier.addTarget(config);
      });
    }

    moduleBuildProfileModifier.outputSync();
  });
}

function configConfigJson(configJsons: ConfigJson[], project: string) {
  configJsons.forEach((configJson) => {
    const singleRichDevices = configJson.deviceTypes && isSubArray(configJson.deviceTypes, Constants.RICH_DEVICES);
    if (singleRichDevices) {
      fse.cpSync(path.resolve(project, configJson.moduleName, 'src', 'main', 'config-bp.json'),
        path.resolve(project, configJson.moduleName, 'src', 'main', 'config.json'),
      );
    }
    const configJsonModifier = new ConfigJsonModifier(project, configJson.moduleName);
    const configJsonObj = configJsonModifier.getObject();
    const module = configJsonObj.module;

    if (singleRichDevices) {
      module.name = configJson.moduleName;
    }

    module.distro.moduleName = configJson.moduleName;
    configJsonModifier.updateModule(module);
    configJson.deviceTypes && configJsonModifier.setDeviceType(configJson.deviceTypes);

    if (configJson.updateAbilities?.length) {
      configJson.updateAbilities.forEach((updateAbility: AbilitiesObj) => {
        configJsonModifier.updateAbility(updateAbility);
      });
    }

    if (configJson.updateJs?.length) {
      configJson.updateJs.forEach((js: JsObj) => {
        configJsonModifier.updateJs(js);
      });
    }

    configJsonModifier.outputSync();
  });
}

function configProject(projectConfig: ProjectConfig, project: string) {
  const projectBuildProfileModifier = new ProjectBuildProfileModifier(project);

  projectConfig.sdkVersion && projectBuildProfileModifier
    .setSdkVersion(projectConfig.sdkVersion)
    .setCompatibleSdkVersion(projectConfig.sdkVersion);

  projectConfig.signConfig && projectBuildProfileModifier.updateSignConfig(projectConfig.signConfig);

  if (projectConfig.updateProducts?.length) {
    projectConfig.updateProducts.forEach((product) => {
      projectBuildProfileModifier.updateProduct(product);
    });
  }

  if (projectConfig.addTargets?.length) {
    projectConfig.addTargets.forEach((addTarget: AddTarget) => {
      projectBuildProfileModifier.updateModuleTargetByName(addTarget.target, addTarget.moduleName);
    });
  }

  if (projectConfig.addModules?.length) {
    projectConfig.addModules.forEach((addModule: ModuleBuildOpt) => {
      projectBuildProfileModifier.updateModule(addModule);
    });
  }

  projectBuildProfileModifier.outputSync();
}

export function configJsProject(project: string, moduleConfigs?: ModuleConfig[],
  configJsons?: ConfigJson[], projectConfig?: ProjectConfig) {
  if (moduleConfigs?.length) {
    configModule(moduleConfigs, project);
  }

  if (configJsons?.length) {
    configConfigJson(configJsons, project);
  }

  if (projectConfig) {
    configProject(projectConfig, project);
  }
}

export function buildTest(projectRoot: string, moduleName: string, targetName: string, task: Tasks,
  option?: BuildOption) {

  const signed = option?.signed === undefined ? true : option.signed;
  const rich = option?.rich === undefined ? false : option.rich;
  const mixed = option?.mixed === undefined ? false : option.mixed;
  const product = option?.product === undefined ? 'default' : option.product;
  const moduleType = option?.moduleType === undefined ? 'entry' : option.moduleType;
  const entryModuleNames = option?.entryModuleNames === undefined ? ['entry'] : option.entryModuleNames;
  const needPackBin = Tasks.ASSEMBLE_HAP && !rich;

  checkBuildResult(projectRoot, moduleName, targetName,
    { needPackBin, signed, task, rich, mixed, product, moduleType, entryModuleNames });
}

export function createPage(projectRoot: string, pageName: string, abilityName = 'MainAbility', moduleName = 'entry') {
  const pagePath = path.resolve(projectRoot, moduleName, 'src', 'main', 'js', abilityName, 'pages', pageName);
  if (fse.pathExistsSync(pagePath)) {
    throw new Error('The page already exists.');
  }

  fse.mkdirsSync(pagePath);
  fse.writeFileSync(path.resolve(pagePath, `${pageName}.css`), PAGE_CSS_TEMPLATE);
  fse.writeFileSync(path.resolve(pagePath, `${pageName}.hml`), PAGE_HML_TEMPLATE);
  fse.writeFileSync(path.resolve(pagePath, `${pageName}.js`), PAGE_JS_TEMPLATE);

  const configJsonModifier = new ConfigJsonModifier(projectRoot, moduleName);
  const configJs = configJsonModifier.getJsByName(`.${abilityName}`);
  if (configJs) {
    configJs.pages.push(`pages/${pageName}/${pageName}`);
    configJsonModifier.updateJs(configJs);
  }

  configJsonModifier.outputSync();
}

export function checkBuildResult(projectRoot: string, moduleName: string, targetName: string, option: CheckOptions) {
  // 检查output目录下文件是否完整
  if (option?.task === Tasks.ASSEMBLE_HAP) {
    if (option.mixed) {
      checkMixedHapOutput(projectRoot, moduleName, targetName, option);
    } else {
      checkHapOutput(projectRoot, moduleName, targetName, option);
    }
  } else {
    if (option.mixed) {
      checkMixedAppOutput(projectRoot, moduleName, targetName, option);
    } else {
      checkAppOutput(projectRoot, moduleName, targetName, option);
    }
  }
}

function checkHapOutput(projectRoot: string, moduleName: string, targetName: string, option: CheckOptions) {
  const probe = new Probes(projectRoot, path.resolve(projectRoot, moduleName), targetName);
  const buildOutputs = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath()));

  let fileList: string[] = [];
  if (option.moduleType === 'entry') {
    fileList = [...Constants.HAP_OUTPUT_FILE_LIST];
    option?.needPackBin && fileList.push('bin');
    option?.signed && fileList.push(Constants.SIGNED_HAP);
    fileList = generateFileName(fileList, { moduleName, targetName });
  } else {
    option.entryModuleNames?.forEach((entryModuleName) => {
      if (option.signed) {
        fileList = fileList.concat(generateFileName([...Constants.FEATURE_HAP_OUTPUT_FILE_LIST],
          { moduleName, targetName, entryModuleName }));
      } else {
        fileList = fileList.concat(generateFileName([...Constants.FEATURE_UNSIGNED_HAP_OUTPUT_FILE_LIST],
          { moduleName, targetName, entryModuleName }));
      }
    });
  }

  expect(buildOutputs).toEqual(fileList.sort());

  // 检查output/bin目录下文件是否完整
  if (option?.needPackBin) {
    const binPath = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath(), 'bin'));
    expect(binPath).toEqual(
      generateFileName(Constants.HAP_OUTPUT_BIN_FILE_LIST,
        { moduleName, targetName }).sort());
  }

  // 检查资源目录是否完整
  if (!option.rich) {
    const liteSourcePath = fse.readdirSync(path.resolve(probe.getIntermediatesLiteSourcePath()));
    expect(liteSourcePath).toEqual(Constants.SOURCE_FILE_LIST.sort());
  }
}

function checkMixedHapOutput(projectRoot: string, moduleName: string, targetName: string, option: CheckOptions) {
  let fileList: string[] = [];
  if (option.moduleType === 'entry') {
    if (option.signed) {
      fileList = generateFileName([...Constants.MIXED_HAP_OUTPUT_FILE_LIST], { moduleName, targetName });
    } else {
      fileList = generateFileName([...Constants.MIXED_UNSIGNED_HAP_OUTPUT_FILE_LIST], { moduleName, targetName });
    }
  } else {
    option.entryModuleNames?.forEach((entryModuleName) => {
      fileList = fileList.concat(generateFileName([...Constants.FEATURE_HAP_OUTPUT_FILE_LIST],
        { moduleName, targetName, entryModuleName }));
    });
  }

  const probe = new Probes(projectRoot, path.resolve(projectRoot, moduleName), targetName);
  const buildOutputs = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath()));
  expect(buildOutputs).toEqual(fileList.sort());

  if (option.moduleType !== 'entry') {
    return;
  }

  const binFiles = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath(), 'bin'));
  expect(binFiles).toEqual([`${moduleName}-${targetName}.bin`].sort());

  const hapFileList = [
    `${moduleName}-${targetName}-unsigned.hap`,
    `${moduleName}-${targetName}-lite-unsigned.hap`,
    'bin',
    'pack.info'
  ];
  option.signed && hapFileList.push(`${moduleName}-${targetName}-signed.hap`);
  option.signed && hapFileList.push(`${moduleName}-${targetName}-lite-signed.hap`);

  const files = fse.readdirSync(probe.getModuleBuildOutputPath());
  expect(files).toEqual(hapFileList.sort());
}

function checkAppOutput(projectRoot: string, moduleName: string, targetName: string, option: CheckOptions) {
  const projectName = path.basename(projectRoot);
  const probe = new Probes(projectRoot, path.resolve(projectRoot, moduleName), targetName);

  const projectBuildOutputs = fse.readdirSync(path.resolve(probe.getProjectOutputPath()));

  const fileList = [...Constants.APP_OUTPUT_FILE_LIST];
  option.signed && fileList.push(Constants.SIGNED_APP);

  expect(projectBuildOutputs).toEqual(generateFileName(fileList, { projectName, targetName }).sort());

  // 检查module output目录下文件是否完整
  const moduleBuildOutputs = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath()));

  const entryFileList = [...Constants.HAP_OUTPUT_FILE_LIST, 'app'];

  option.signed && entryFileList.push(Constants.SIGNED_HAP);
  expect(moduleBuildOutputs).toEqual(generateFileName(entryFileList,
    { projectName, moduleName, targetName }).sort());

  // 检查module output/app目录下文件是否完整
  const moduleBuildApps = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath(), 'app'));
  expect(moduleBuildApps).toEqual(generateFileName(Constants.HAP_OUTPUT_APP_FILE_LIST,
    { moduleName, targetName }).sort());
}

function checkMixedAppOutput(projectRoot: string, moduleName: string, targetName: string, option: CheckOptions) {
  const projectName = path.basename(projectRoot);
  const probe = new Probes(projectRoot, path.resolve(projectRoot, moduleName), targetName);

  const projectBuildOutputs = fse.readdirSync(path.resolve(probe.getProjectOutputPath()));

  const fileList = [...Constants.APP_OUTPUT_FILE_LIST];
  option.signed && fileList.push(Constants.SIGNED_APP);

  expect(projectBuildOutputs).toEqual(generateFileName(fileList, { projectName, targetName }).sort());

  // 检查module output目录下文件是否完整
  const moduleBuildOutputs = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath()));

  let outputFileList: string[];
  if (option.signed) {
    outputFileList = [...Constants.MIXED_APP_OUTPUT_FILE_LIST];
  } else {
    outputFileList = [...Constants.MIXED_UNSIGNED_APP_OUTPUT_FILE_LIST];
  }

  expect(moduleBuildOutputs).toEqual(outputFileList.sort());

  // 检查module output/app目录下文件是否完整
  const appLiteOutputFileList = fse.readdirSync(path.resolve(probe.getModuleBuildOutputPath(), 'app'));
  expect(appLiteOutputFileList).toEqual([
    `${moduleName}-${targetName}-lite.hap`,
    `${moduleName}-${targetName}.hap`,
  ].sort());
}

function generateFileName(fileList: string[],
  option: {
    projectName?: string, moduleName?: string, targetName?: string, productName?: string,
    entryModuleName?: string
  }) {
  return fileList.map((file: string) => {
    if (option.projectName) {
      file = file.replace('{projectName}', option.projectName);
    }

    if (option.moduleName) {
      file = file.replace('{moduleName}', option.moduleName);
    }

    if (option.targetName) {
      file = file.replace('{targetName}', option.targetName);
    }

    if (option.entryModuleName) {
      file = file.replace('{entryModuleName}', option.entryModuleName);
    }

    file = file.replace('{productName}', option.productName || 'default');

    return file;
  });
}

export function createModule(projectRoot: string, moduleName: string, deviceType: string[], abilityName: string,
  moduleType: string, entryModules: string[] = ['entry']) {
  const entryPath = path.resolve(projectRoot, 'entry');
  const featurePath = path.resolve(projectRoot, moduleName);

  fse.copySync(entryPath, featurePath);

  const configJsonModifier = new ConfigJsonModifier(projectRoot, moduleName);

  const abilityObj = configJsonModifier.getAbilityByName('.MainAbility') || {
    orientation: 'unspecified',
    formsEnabled: false,
    name: `.${abilityName}`,
    srcLanguage: 'js',
    srcPath: abilityName,
    icon: '$media:icon',
    description: '$string:MainAbility_desc',
    label: '$string:MainAbility_label',
    type: 'page',
    visible: true,
    launchType: 'standard',
  };
  abilityObj.name = `.${abilityName}`;
  abilityObj.srcPath = abilityName;

  configJsonModifier.updateModule(
    {
      package: `com.example.${moduleName}`,
      name: `${moduleName}`,
      mainAbility: `.${abilityName}`,
      deviceType: deviceType,
      distro: {
        deliveryWithInstall: true,
        moduleName: moduleName,
        moduleType: moduleType,
        installationFree: false,
      },
      abilities: [abilityObj],
    });

  const jsObj = configJsonModifier.getJsByName('.MainAbility') || {
    'pages': [
      'pages/index/index',
    ],
    'name': `.${abilityName}`,
    'window': {
      'designWidth': 720,
      'autoDesignWidth': false,
    },
  };

  jsObj.name = `.${abilityName}`;
  configJsonModifier.updateJs(jsObj);

  configJsonModifier.outputSync();

  const moduleBuildProfileModifier = new ModuleBuildProfileModifier(projectRoot, moduleName);
  moduleBuildProfileModifier.setEntryModules(entryModules);
  moduleBuildProfileModifier.outputSync();

  const projectBuildProfileModifier = new ProjectBuildProfileModifier(projectRoot);
  projectBuildProfileModifier.updateModule({
    'name': moduleName,
    'srcPath': `./${moduleName}`,
    'targets': [
      {
        'name': 'default',
        'applyToProducts': [
          'default',
        ],
      },
    ],
  });
  projectBuildProfileModifier.outputSync();
}
